package modeller.queueingnetworksmodel.probabilities;

import modeller.databasedesignmodel.DatabaseDesign;
import modeller.databasedesignmodel.relation.Relation;
import modeller.databasedesignmodel.transaction.*;

import java.util.*;

import static queped.NeptuneHelperFunctions.isInDebugMode;

/**
 * Created by:  Jason Ye
 * Date:        30/05/2012
 * Time:        10:37
 */
public class TransactionRoutingMatrix {

    // record of which index each relation is at
    private HashMap<Relation, Integer> mapRelationToIndex;
    private Relation[] mapIndexToRelation;
    private HashSet<Relation> relations;
    private HashSet<Transaction> transactions;
    private HashMap<Transaction, double[][]> routingProbabilities;

    private static final int nil = 0;

    public TransactionRoutingMatrix(HashSet<Transaction> transactions, HashSet<Relation> relations, HashMap<Relation, Integer> mapRelationToIndex) {

        this(transactions, relations);

    }

    public TransactionRoutingMatrix(DatabaseDesign design) {

        this(design.getTransactions(), design.getRelations());

    }

    private void generateMapIndexToRelation() {
        for (Relation r : mapRelationToIndex.keySet()) {
            mapIndexToRelation[mapRelationToIndex.get(r)] = r;
        }
    }

    public TransactionRoutingMatrix(HashSet<Transaction> transactions, HashSet<Relation> relations) {

        this.transactions = transactions;
        this.relations = relations;
        this.mapRelationToIndex = constructRelationIndex(relations);

        this.mapIndexToRelation = new Relation[relations.size()];
        // construct array of relations and create index of their position in the array
        generateMapIndexToRelation();

        this.routingProbabilities = generateRoutingProbabilities();
    }


    public double getTransitionProbability(Transaction t, Relation source, Relation dest) {

        double[][] pForTrans = routingProbabilities.get(t);

        if (dest.getRelationName().equals("Router")) {
            return pForTrans[mapRelationToIndex.get(source)][0];

        } else if (source.getRelationName().equals("Router")) {
            return pForTrans[0][mapRelationToIndex.get(dest)];

        } else {
            return pForTrans[mapRelationToIndex.get(source)][mapRelationToIndex.get(dest)];
        }
    }

    /**
     * implement Algorithm 3.1 from Rasha Osman's thesis
     *
     * @return routing proababilities for each Transaction
     */
    private HashMap<Transaction, double[][]> generateRoutingProbabilities() {


        // implememnt algorithm 3.1 from Rasha Osman's thesis

        // init transition matrix
        HashMap<Transaction, double[][]> transitions = new HashMap<Transaction, double[][]>();

        // for each transaction include a new entry in the return HashMap
        for (Transaction transaction : transactions) {

            // specify currentTable = current table in path of transaction = nil
            int currentTable = nil;

            // init branch[2] = vector of last table accessed by true branch (branch[1]) and false branch (branch[0])
            int branch[] = {nil, nil};
            // init bran_table = current table of branch statement = nil
            int bran_table = nil;
            // init prev_branch = vector holding the initial value of branch[]
            int prev_branch[] = Arrays.copyOf(branch, branch.length);
            //init routing table - getFirst element is nil
            double[][] p = new double[relations.size() + 1][relations.size() + 1];
            for (int i = 0; i < p.length; i++) {
                for (int j = 0; j < p.length; j++) {
                    p[i][j] = nil;
                }
            }

            //init TransactionState
            TransactionRoutingAlgoState state = new TransactionRoutingAlgoState(currentTable, branch, bran_table, prev_branch, p);

            // create iterator
            LinkedList<IProceduralStatement> proceduralStatements = transaction.getProceduralStatements();
            Iterator<IProceduralStatement> it = proceduralStatements.listIterator();

            // for all s in t.S
            while (it.hasNext()) {

                IProceduralStatement s = it.next();

                if (s instanceof SQLQuerySequence) {

                    for (SQLQuery q : ((SQLQuerySequence) s).getStatementSequence()) {
                        state = processQuery(q, state);
                        printP(state);
                    }

                } else if (s instanceof Branch) {

                    state = processBranch((Branch) s, state);
                    printP(state);
                } else { //loop
                    state = processLoop((Loop) s, state);
                    printP(state);
                }

            }

            // set p[i][i]=0: eg, no transitions to self

            for (int i = 0; i < p.length; i++) {
                state.setP(i, i, 0);
            }

            // if s is the last statement in t, then transaction leaves network, all transitions go to nil
            state.terminate();
            printP(state);
            // add to hashmap
            transitions.put(transaction, state.getP());
        }


        // return transition matrix
        return transitions;

    }

    private TransactionRoutingAlgoState processLoop(Loop s, TransactionRoutingAlgoState state) {

        SQLQuerySequence seq = (SQLQuerySequence) s.getLoopContents();

        boolean isFirstSQLStatementInQuery = true;
        // for all q in loop
        for (SQLQuery q : seq.getStatementSequence()) {

            // for all r in q.Access -- irreleveant, only one relation per query

            Relation r = q.getRelation().getFirst();
            int rIndex = findRelationIndex(r);
            //if q is getFirst SQL statement and r is getFirst table accessed by q
            if (isFirstSQLStatementInQuery) {

                isFirstSQLStatementInQuery = false;
                // connect branch statement to getFirst table of this SQL statement
                state.connectLastTableFromBranchToThisRelation(rIndex);
                // P[c,branch,r]=1
                //branch[]=nil
                state.setBranchToNil();
                // current_table=r
                state.setCurrentTable(rIndex);
            } else {// else
                //P[c,current_table, r]=1
                state.setP(state.getCurrentTable(), rIndex, 1);
                // current_table<- r
                state.setCurrentTable(rIndex);
            }// end if
            // end for

        }// end for

        return state;
    }


    private TransactionRoutingAlgoState processBranch(Branch b, TransactionRoutingAlgoState state) {

        //prev_branch[]=branch[]
        state.setPrevBranchToCurrentBranch();

        // assume branches can only hold queries
        SQLQuerySequence trueB = (SQLQuerySequence) b.getTrueStatement();
        SQLQuerySequence falseB = (SQLQuerySequence) b.getFalseStatement();
        double trueBranchPi = b.getTrueBranchExecProbability();

        int[] branch = state.getBranch();

        // for i in n do (total number of branches)
        for (int i = 0; i < branch.length; i++) {

            SQLQuerySequence thisB = (i == 1) ? trueB : falseB;

            //bran_table= current_table
            state.setBranTableToCurrentTable();

            boolean isFirstSQLStatement = true;
            //for all q in branch[i]
            for (SQLQuery q : thisB.getStatementSequence()) {

                //for all r in q.access - only one relation per query
                Relation r = q.getRelation().getFirst();
                int rIndex = findRelationIndex(r);
                // if [ (q is getFirst SQL statement)
                //    and (r is getFirst table accessed by q = always true) ] then

                if (isFirstSQLStatement) {

                    isFirstSQLStatement = false;

                    double pi = (i == 1) ? trueBranchPi : 1 - trueBranchPi;

                    //if prev_branch!= nil then
                    if (!state.isPrevBranchNil()) {
                        // connect last table accessed by previous branches to getFirst table of this branch's SQL,
                        // with branch probability pi

                        // for prev_branch[1] to n do
                        state.connectLastTableAccessedByPrevBranchToThisSQL(rIndex, pi);
                        //  P [ c, prev_branch[], r ] = pi
                        // end for

                        // bran_table <- r
                        state.setBran_table(rIndex);

                    } else {// else

                        // P[ c, bran_table, r] <- pi
                        state.setP(state.getBran_table(), rIndex, pi);
                        // bran_table<-r
                        state.setBran_table(rIndex);
                    }// end if


                } else {// else
                    // P[c,bran_table,r]=1
                    state.setP(state.getBran_table(), rIndex, 1);
                    // bran_table <- r
                    state.setBran_table(rIndex);
                }// end if

                //branch[i]=r
                if (i == 1) {
                    state.setTrueBranch(rIndex);
                } else {
                    state.setFalseBranch(rIndex);
                }
                // end for all r - irrelevant because only one relation per query

            } // end for all q


        }// end for

        // prev_branch <- nil
        state.setPrevBranchToNil();

        return state;

    }

    private TransactionRoutingAlgoState processQuery(SQLQuery q, TransactionRoutingAlgoState state) {

        Relation r = q.getRelation().getFirst();
        int rIndex = findRelationIndex(r);

        // if branch!= nil (and relation is getFirst accessed, but queries assumed to only accessed one relation), then
        if (!state.isBranchNil()) {

            // connect last table accessed from previous branch statement to getFirst table of this SQL statement
            state.connectLastTableFromBranchToThisRelation(rIndex);
            // branch[] <- nil
            state.setBranchToNil();
            // current_table <- r
            state.setCurrentTable(rIndex);
        } else {

            // P[c,current_table,r] = 1
            state.setP(state.getCurrentTable(), rIndex, 1);
            //current_table <- r
            state.setCurrentTable(rIndex);
        } // end if

        return state;

    }


    private int findRelationIndex(Relation r) {
        return mapRelationToIndex.get(r);
    }

    public HashMap<Relation, Integer> getMapRelationToIndex() {
        return mapRelationToIndex;
    }

    public HashSet<Relation> getRelations() {
        return relations;
    }

    public HashSet<Transaction> getTransactions() {
        return transactions;
    }

    public HashMap<Transaction, double[][]> getRoutingProbabilities() {
        return routingProbabilities;
    }

    public Relation[] getMapIndexToRelation() {
        return mapIndexToRelation;
    }

    public void printP(TransactionRoutingAlgoState state) {

        if (isInDebugMode()) {


            System.out.println("Start p");
            double[][] p = state.getP();
            for (int i = 0; i < p.length; i++) {
                for (int j = 0; j < p.length; j++) {
                    System.out.print(p[i][j] + "\t\t\t");
                }
                System.out.println();
            }

            System.out.println();
            System.out.println();


            System.out.println("end p");
        }
    }

    /**
     * generates a lookup table between relations and index for the instantaneous transition probability generator
     *
     * @param relations
     * @return
     */
    private HashMap<Relation, Integer> constructRelationIndex(HashSet<Relation> relations) {

        mapRelationToIndex = new HashMap<Relation, Integer>();
        int currentIndex = 1;
        for (Relation r : relations) {
            if (r.getRelationName().equals("Router") && r.getBNumDbPages() == 0) {
                mapRelationToIndex.put(r, 0);

            } else {
                mapRelationToIndex.put(r, currentIndex);
                currentIndex++;
            }
        }

        return mapRelationToIndex;

    }

}
